1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 package org.apache.commons.lang3.time;
18
19 import java.io.IOException;
20 import java.io.ObjectInputStream;
21 import java.io.Serializable;
22 import java.text.DateFormatSymbols;
23 import java.text.ParseException;
24 import java.text.ParsePosition;
25 import java.util.ArrayList;
26 import java.util.Calendar;
27 import java.util.Date;
28 import java.util.HashMap;
29 import java.util.List;
30 import java.util.Locale;
31 import java.util.Map;
32 import java.util.SortedMap;
33 import java.util.TimeZone;
34 import java.util.TreeMap;
35 import java.util.concurrent.ConcurrentHashMap;
36 import java.util.concurrent.ConcurrentMap;
37 import java.util.regex.Matcher;
38 import java.util.regex.Pattern;
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72 public class FastDateParser implements DateParser, Serializable {
73
74
75
76
77
78 private static final long serialVersionUID = 2L;
79
80 static final Locale JAPANESE_IMPERIAL = new Locale("ja","JP","JP");
81
82
83 private final String pattern;
84 private final TimeZone timeZone;
85 private final Locale locale;
86 private final int century;
87 private final int startYear;
88
89
90 private transient Pattern parsePattern;
91 private transient Strategy[] strategies;
92
93
94 private transient String currentFormatField;
95 private transient Strategy nextStrategy;
96
97
98
99
100
101
102
103
104
105
106
107
108 protected FastDateParser(final String pattern, final TimeZone timeZone, final Locale locale) {
109 this(pattern, timeZone, locale, null);
110 }
111
112
113
114
115
116
117
118
119
120
121
122
123 protected FastDateParser(final String pattern, final TimeZone timeZone, final Locale locale, final Date centuryStart) {
124 this.pattern = pattern;
125 this.timeZone = timeZone;
126 this.locale = locale;
127
128 final Calendar definingCalendar = Calendar.getInstance(timeZone, locale);
129 int centuryStartYear;
130 if(centuryStart!=null) {
131 definingCalendar.setTime(centuryStart);
132 centuryStartYear= definingCalendar.get(Calendar.YEAR);
133 }
134 else if(locale.equals(JAPANESE_IMPERIAL)) {
135 centuryStartYear= 0;
136 }
137 else {
138
139 definingCalendar.setTime(new Date());
140 centuryStartYear= definingCalendar.get(Calendar.YEAR)-80;
141 }
142 century= centuryStartYear / 100 * 100;
143 startYear= centuryStartYear - century;
144
145 init(definingCalendar);
146 }
147
148
149
150
151
152
153
154 private void init(final Calendar definingCalendar) {
155
156 final StringBuilder regex= new StringBuilder();
157 final List<Strategy> collector = new ArrayList<Strategy>();
158
159 final Matcher patternMatcher= formatPattern.matcher(pattern);
160 if(!patternMatcher.lookingAt()) {
161 throw new IllegalArgumentException(
162 "Illegal pattern character '" + pattern.charAt(patternMatcher.regionStart()) + "'");
163 }
164
165 currentFormatField= patternMatcher.group();
166 Strategy currentStrategy= getStrategy(currentFormatField, definingCalendar);
167 for(;;) {
168 patternMatcher.region(patternMatcher.end(), patternMatcher.regionEnd());
169 if(!patternMatcher.lookingAt()) {
170 nextStrategy = null;
171 break;
172 }
173 final String nextFormatField= patternMatcher.group();
174 nextStrategy = getStrategy(nextFormatField, definingCalendar);
175 if(currentStrategy.addRegex(this, regex)) {
176 collector.add(currentStrategy);
177 }
178 currentFormatField= nextFormatField;
179 currentStrategy= nextStrategy;
180 }
181 if (patternMatcher.regionStart() != patternMatcher.regionEnd()) {
182 throw new IllegalArgumentException("Failed to parse \""+pattern+"\" ; gave up at index "+patternMatcher.regionStart());
183 }
184 if(currentStrategy.addRegex(this, regex)) {
185 collector.add(currentStrategy);
186 }
187 currentFormatField= null;
188 strategies= collector.toArray(new Strategy[collector.size()]);
189 parsePattern= Pattern.compile(regex.toString());
190 }
191
192
193
194
195
196
197 @Override
198 public String getPattern() {
199 return pattern;
200 }
201
202
203
204
205 @Override
206 public TimeZone getTimeZone() {
207 return timeZone;
208 }
209
210
211
212
213 @Override
214 public Locale getLocale() {
215 return locale;
216 }
217
218
219
220
221
222
223 Pattern getParsePattern() {
224 return parsePattern;
225 }
226
227
228
229
230
231
232
233
234
235 @Override
236 public boolean equals(final Object obj) {
237 if (! (obj instanceof FastDateParser) ) {
238 return false;
239 }
240 final FastDateParser other = (FastDateParser) obj;
241 return pattern.equals(other.pattern)
242 && timeZone.equals(other.timeZone)
243 && locale.equals(other.locale);
244 }
245
246
247
248
249
250
251 @Override
252 public int hashCode() {
253 return pattern.hashCode() + 13 * (timeZone.hashCode() + 13 * locale.hashCode());
254 }
255
256
257
258
259
260
261 @Override
262 public String toString() {
263 return "FastDateParser[" + pattern + "," + locale + "," + timeZone.getID() + "]";
264 }
265
266
267
268
269
270
271
272
273
274
275
276 private void readObject(final ObjectInputStream in) throws IOException, ClassNotFoundException {
277 in.defaultReadObject();
278
279 final Calendar definingCalendar = Calendar.getInstance(timeZone, locale);
280 init(definingCalendar);
281 }
282
283
284
285
286 @Override
287 public Object parseObject(final String source) throws ParseException {
288 return parse(source);
289 }
290
291
292
293
294 @Override
295 public Date parse(final String source) throws ParseException {
296 final Date date= parse(source, new ParsePosition(0));
297 if(date==null) {
298
299 if (locale.equals(JAPANESE_IMPERIAL)) {
300 throw new ParseException(
301 "(The " +locale + " locale does not support dates before 1868 AD)\n" +
302 "Unparseable date: \""+source+"\" does not match "+parsePattern.pattern(), 0);
303 }
304 throw new ParseException("Unparseable date: \""+source+"\" does not match "+parsePattern.pattern(), 0);
305 }
306 return date;
307 }
308
309
310
311
312 @Override
313 public Object parseObject(final String source, final ParsePosition pos) {
314 return parse(source, pos);
315 }
316
317
318
319
320
321
322
323
324
325
326
327
328
329 @Override
330 public Date parse(final String source, final ParsePosition pos) {
331 final int offset= pos.getIndex();
332 final Matcher matcher= parsePattern.matcher(source.substring(offset));
333 if(!matcher.lookingAt()) {
334 return null;
335 }
336
337 final Calendar cal= Calendar.getInstance(timeZone, locale);
338 cal.clear();
339
340 for(int i=0; i<strategies.length;) {
341 final Strategy strategy= strategies[i++];
342 strategy.setCalendar(this, cal, matcher.group(i));
343 }
344 pos.setIndex(offset+matcher.end());
345 return cal.getTime();
346 }
347
348
349
350
351
352
353
354
355
356
357
358 private static StringBuilder escapeRegex(final StringBuilder regex, final String value, final boolean unquote) {
359 regex.append("\\Q");
360 for(int i= 0; i<value.length(); ++i) {
361 char c= value.charAt(i);
362 switch(c) {
363 case '\'':
364 if(unquote) {
365 if(++i==value.length()) {
366 return regex;
367 }
368 c= value.charAt(i);
369 }
370 break;
371 case '\\':
372 if(++i==value.length()) {
373 break;
374 }
375
376
377
378
379
380
381
382 regex.append(c);
383 c = value.charAt(i);
384 if (c == 'E') {
385 regex.append("E\\\\E\\");
386 c = 'Q';
387 }
388 break;
389 default:
390 break;
391 }
392 regex.append(c);
393 }
394 regex.append("\\E");
395 return regex;
396 }
397
398
399
400
401
402
403
404
405
406 private static Map<String, Integer> getDisplayNames(final int field, final Calendar definingCalendar, final Locale locale) {
407 return definingCalendar.getDisplayNames(field, Calendar.ALL_STYLES, locale);
408 }
409
410
411
412
413
414
415 private int adjustYear(final int twoDigitYear) {
416 final int trial= century + twoDigitYear;
417 return twoDigitYear>=startYear ?trial :trial+100;
418 }
419
420
421
422
423
424 boolean isNextNumber() {
425 return nextStrategy!=null && nextStrategy.isNumber();
426 }
427
428
429
430
431
432 int getFieldWidth() {
433 return currentFormatField.length();
434 }
435
436
437
438
439 private static abstract class Strategy {
440
441
442
443
444
445
446
447 boolean isNumber() {
448 return false;
449 }
450
451
452
453
454
455
456
457
458
459
460 void setCalendar(final FastDateParser parser, final Calendar cal, final String value) {
461
462 }
463
464
465
466
467
468
469
470
471
472 abstract boolean addRegex(FastDateParser parser, StringBuilder regex);
473
474 }
475
476
477
478
479 private static final Pattern formatPattern= Pattern.compile(
480 "D+|E+|F+|G+|H+|K+|M+|S+|W+|X+|Z+|a+|d+|h+|k+|m+|s+|w+|y+|z+|''|'[^']++(''[^']*+)*+'|[^'A-Za-z]++");
481
482
483
484
485
486
487
488 private Strategy getStrategy(final String formatField, final Calendar definingCalendar) {
489 switch(formatField.charAt(0)) {
490 case '\'':
491 if(formatField.length()>2) {
492 return new CopyQuotedStrategy(formatField.substring(1, formatField.length()-1));
493 }
494
495 default:
496 return new CopyQuotedStrategy(formatField);
497 case 'D':
498 return DAY_OF_YEAR_STRATEGY;
499 case 'E':
500 return getLocaleSpecificStrategy(Calendar.DAY_OF_WEEK, definingCalendar);
501 case 'F':
502 return DAY_OF_WEEK_IN_MONTH_STRATEGY;
503 case 'G':
504 return getLocaleSpecificStrategy(Calendar.ERA, definingCalendar);
505 case 'H':
506 return HOUR_OF_DAY_STRATEGY;
507 case 'K':
508 return HOUR_STRATEGY;
509 case 'M':
510 return formatField.length()>=3 ?getLocaleSpecificStrategy(Calendar.MONTH, definingCalendar) :NUMBER_MONTH_STRATEGY;
511 case 'S':
512 return MILLISECOND_STRATEGY;
513 case 'W':
514 return WEEK_OF_MONTH_STRATEGY;
515 case 'a':
516 return getLocaleSpecificStrategy(Calendar.AM_PM, definingCalendar);
517 case 'd':
518 return DAY_OF_MONTH_STRATEGY;
519 case 'h':
520 return HOUR12_STRATEGY;
521 case 'k':
522 return HOUR24_OF_DAY_STRATEGY;
523 case 'm':
524 return MINUTE_STRATEGY;
525 case 's':
526 return SECOND_STRATEGY;
527 case 'w':
528 return WEEK_OF_YEAR_STRATEGY;
529 case 'y':
530 return formatField.length()>2 ?LITERAL_YEAR_STRATEGY :ABBREVIATED_YEAR_STRATEGY;
531 case 'X':
532 return ISO8601TimeZoneStrategy.getStrategy(formatField.length());
533 case 'Z':
534 if (formatField.equals("ZZ")) {
535 return ISO_8601_STRATEGY;
536 }
537
538 case 'z':
539 return getLocaleSpecificStrategy(Calendar.ZONE_OFFSET, definingCalendar);
540 }
541 }
542
543 @SuppressWarnings("unchecked")
544 private static final ConcurrentMap<Locale, Strategy>[] caches = new ConcurrentMap[Calendar.FIELD_COUNT];
545
546
547
548
549
550
551 private static ConcurrentMap<Locale, Strategy> getCache(final int field) {
552 synchronized(caches) {
553 if(caches[field]==null) {
554 caches[field]= new ConcurrentHashMap<Locale,Strategy>(3);
555 }
556 return caches[field];
557 }
558 }
559
560
561
562
563
564
565
566 private Strategy getLocaleSpecificStrategy(final int field, final Calendar definingCalendar) {
567 final ConcurrentMap<Locale,Strategy> cache = getCache(field);
568 Strategy strategy= cache.get(locale);
569 if(strategy==null) {
570 strategy= field==Calendar.ZONE_OFFSET
571 ? new TimeZoneStrategy(locale)
572 : new CaseInsensitiveTextStrategy(field, definingCalendar, locale);
573 final Strategy inCache= cache.putIfAbsent(locale, strategy);
574 if(inCache!=null) {
575 return inCache;
576 }
577 }
578 return strategy;
579 }
580
581
582
583
584 private static class CopyQuotedStrategy extends Strategy {
585 private final String formatField;
586
587
588
589
590
591 CopyQuotedStrategy(final String formatField) {
592 this.formatField= formatField;
593 }
594
595
596
597
598 @Override
599 boolean isNumber() {
600 char c= formatField.charAt(0);
601 if(c=='\'') {
602 c= formatField.charAt(1);
603 }
604 return Character.isDigit(c);
605 }
606
607
608
609
610 @Override
611 boolean addRegex(final FastDateParser parser, final StringBuilder regex) {
612 escapeRegex(regex, formatField, true);
613 return false;
614 }
615 }
616
617
618
619
620 private static class CaseInsensitiveTextStrategy extends Strategy {
621 private final int field;
622 private final Locale locale;
623 private final Map<String, Integer> lKeyValues;
624
625
626
627
628
629
630
631 CaseInsensitiveTextStrategy(final int field, final Calendar definingCalendar, final Locale locale) {
632 this.field= field;
633 this.locale= locale;
634 final Map<String, Integer> keyValues = getDisplayNames(field, definingCalendar, locale);
635 this.lKeyValues= new HashMap<String,Integer>();
636
637 for(final Map.Entry<String, Integer> entry : keyValues.entrySet()) {
638 lKeyValues.put(entry.getKey().toLowerCase(locale), entry.getValue());
639 }
640 }
641
642
643
644
645 @Override
646 boolean addRegex(final FastDateParser parser, final StringBuilder regex) {
647 regex.append("((?iu)");
648 for(final String textKeyValue : lKeyValues.keySet()) {
649 escapeRegex(regex, textKeyValue, false).append('|');
650 }
651 regex.setCharAt(regex.length()-1, ')');
652 return true;
653 }
654
655
656
657
658 @Override
659 void setCalendar(final FastDateParser parser, final Calendar cal, final String value) {
660 final Integer iVal = lKeyValues.get(value.toLowerCase(locale));
661 if(iVal == null) {
662 final StringBuilder sb= new StringBuilder(value);
663 sb.append(" not in (");
664 for(final String textKeyValue : lKeyValues.keySet()) {
665 sb.append(textKeyValue).append(' ');
666 }
667 sb.setCharAt(sb.length()-1, ')');
668 throw new IllegalArgumentException(sb.toString());
669 }
670 cal.set(field, iVal.intValue());
671 }
672 }
673
674
675
676
677
678 private static class NumberStrategy extends Strategy {
679 private final int field;
680
681
682
683
684
685 NumberStrategy(final int field) {
686 this.field= field;
687 }
688
689
690
691
692 @Override
693 boolean isNumber() {
694 return true;
695 }
696
697
698
699
700 @Override
701 boolean addRegex(final FastDateParser parser, final StringBuilder regex) {
702
703 if(parser.isNextNumber()) {
704 regex.append("(\\p{Nd}{").append(parser.getFieldWidth()).append("}+)");
705 }
706 else {
707 regex.append("(\\p{Nd}++)");
708 }
709 return true;
710 }
711
712
713
714
715 @Override
716 void setCalendar(final FastDateParser parser, final Calendar cal, final String value) {
717 cal.set(field, modify(Integer.parseInt(value)));
718 }
719
720
721
722
723
724
725 int modify(final int iValue) {
726 return iValue;
727 }
728 }
729
730 private static final Strategy ABBREVIATED_YEAR_STRATEGY = new NumberStrategy(Calendar.YEAR) {
731
732
733
734 @Override
735 void setCalendar(final FastDateParser parser, final Calendar cal, final String value) {
736 int iValue= Integer.parseInt(value);
737 if(iValue<100) {
738 iValue= parser.adjustYear(iValue);
739 }
740 cal.set(Calendar.YEAR, iValue);
741 }
742 };
743
744
745
746
747 private static class TimeZoneStrategy extends Strategy {
748
749 private final String validTimeZoneChars;
750 private final SortedMap<String, TimeZone> tzNames= new TreeMap<String, TimeZone>(String.CASE_INSENSITIVE_ORDER);
751
752
753
754
755 private static final int ID = 0;
756
757
758
759 private static final int LONG_STD = 1;
760
761
762
763 private static final int SHORT_STD = 2;
764
765
766
767 private static final int LONG_DST = 3;
768
769
770
771 private static final int SHORT_DST = 4;
772
773
774
775
776
777 TimeZoneStrategy(final Locale locale) {
778 final String[][] zones = DateFormatSymbols.getInstance(locale).getZoneStrings();
779 for (final String[] zone : zones) {
780 if (zone[ID].startsWith("GMT")) {
781 continue;
782 }
783 final TimeZone tz = TimeZone.getTimeZone(zone[ID]);
784 if (!tzNames.containsKey(zone[LONG_STD])){
785 tzNames.put(zone[LONG_STD], tz);
786 }
787 if (!tzNames.containsKey(zone[SHORT_STD])){
788 tzNames.put(zone[SHORT_STD], tz);
789 }
790 if (tz.useDaylightTime()) {
791 if (!tzNames.containsKey(zone[LONG_DST])){
792 tzNames.put(zone[LONG_DST], tz);
793 }
794 if (!tzNames.containsKey(zone[SHORT_DST])){
795 tzNames.put(zone[SHORT_DST], tz);
796 }
797 }
798 }
799
800 final StringBuilder sb= new StringBuilder();
801 sb.append("(GMT[+-]\\d{1,2}:\\d{2}").append('|');
802 sb.append("[+-]\\d{4}").append('|');
803 for(final String id : tzNames.keySet()) {
804 escapeRegex(sb, id, false).append('|');
805 }
806 sb.setCharAt(sb.length()-1, ')');
807 validTimeZoneChars= sb.toString();
808 }
809
810
811
812
813 @Override
814 boolean addRegex(final FastDateParser parser, final StringBuilder regex) {
815 regex.append(validTimeZoneChars);
816 return true;
817 }
818
819
820
821
822 @Override
823 void setCalendar(final FastDateParser parser, final Calendar cal, final String value) {
824 TimeZone tz;
825 if(value.charAt(0)=='+' || value.charAt(0)=='-') {
826 tz= TimeZone.getTimeZone("GMT"+value);
827 }
828 else if(value.startsWith("GMT")) {
829 tz= TimeZone.getTimeZone(value);
830 }
831 else {
832 tz= tzNames.get(value);
833 if(tz==null) {
834 throw new IllegalArgumentException(value + " is not a supported timezone name");
835 }
836 }
837 cal.setTimeZone(tz);
838 }
839 }
840
841 private static class ISO8601TimeZoneStrategy extends Strategy {
842
843 private final String pattern;
844
845
846
847
848
849 ISO8601TimeZoneStrategy(String pattern) {
850 this.pattern = pattern;
851 }
852
853
854
855
856 @Override
857 boolean addRegex(FastDateParser parser, StringBuilder regex) {
858 regex.append(pattern);
859 return true;
860 }
861
862
863
864
865 @Override
866 void setCalendar(FastDateParser parser, Calendar cal, String value) {
867 if (value.equals("Z")) {
868 cal.setTimeZone(TimeZone.getTimeZone("UTC"));
869 } else {
870 cal.setTimeZone(TimeZone.getTimeZone("GMT" + value));
871 }
872 }
873
874 private static final Strategy ISO_8601_1_STRATEGY = new ISO8601TimeZoneStrategy("(Z|(?:[+-]\\d{2}))");
875 private static final Strategy ISO_8601_2_STRATEGY = new ISO8601TimeZoneStrategy("(Z|(?:[+-]\\d{2}\\d{2}))");
876 private static final Strategy ISO_8601_3_STRATEGY = new ISO8601TimeZoneStrategy("(Z|(?:[+-]\\d{2}(?::)\\d{2}))");
877
878
879
880
881
882
883
884
885 static Strategy getStrategy(int tokenLen) {
886 switch(tokenLen) {
887 case 1:
888 return ISO_8601_1_STRATEGY;
889 case 2:
890 return ISO_8601_2_STRATEGY;
891 case 3:
892 return ISO_8601_3_STRATEGY;
893 default:
894 throw new IllegalArgumentException("invalid number of X");
895 }
896 }
897 }
898
899 private static final Strategy NUMBER_MONTH_STRATEGY = new NumberStrategy(Calendar.MONTH) {
900 @Override
901 int modify(final int iValue) {
902 return iValue-1;
903 }
904 };
905 private static final Strategy LITERAL_YEAR_STRATEGY = new NumberStrategy(Calendar.YEAR);
906 private static final Strategy WEEK_OF_YEAR_STRATEGY = new NumberStrategy(Calendar.WEEK_OF_YEAR);
907 private static final Strategy WEEK_OF_MONTH_STRATEGY = new NumberStrategy(Calendar.WEEK_OF_MONTH);
908 private static final Strategy DAY_OF_YEAR_STRATEGY = new NumberStrategy(Calendar.DAY_OF_YEAR);
909 private static final Strategy DAY_OF_MONTH_STRATEGY = new NumberStrategy(Calendar.DAY_OF_MONTH);
910 private static final Strategy DAY_OF_WEEK_IN_MONTH_STRATEGY = new NumberStrategy(Calendar.DAY_OF_WEEK_IN_MONTH);
911 private static final Strategy HOUR_OF_DAY_STRATEGY = new NumberStrategy(Calendar.HOUR_OF_DAY);
912 private static final Strategy HOUR24_OF_DAY_STRATEGY = new NumberStrategy(Calendar.HOUR_OF_DAY) {
913 @Override
914 int modify(final int iValue) {
915 return iValue == 24 ? 0 : iValue;
916 }
917 };
918 private static final Strategy HOUR12_STRATEGY = new NumberStrategy(Calendar.HOUR) {
919 @Override
920 int modify(final int iValue) {
921 return iValue == 12 ? 0 : iValue;
922 }
923 };
924 private static final Strategy HOUR_STRATEGY = new NumberStrategy(Calendar.HOUR);
925 private static final Strategy MINUTE_STRATEGY = new NumberStrategy(Calendar.MINUTE);
926 private static final Strategy SECOND_STRATEGY = new NumberStrategy(Calendar.SECOND);
927 private static final Strategy MILLISECOND_STRATEGY = new NumberStrategy(Calendar.MILLISECOND);
928 private static final Strategy ISO_8601_STRATEGY = new ISO8601TimeZoneStrategy("(Z|(?:[+-]\\d{2}(?::?\\d{2})?))");
929
930
931 }